﻿using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Runtime.InteropServices;
using System.Globalization;
using System.Diagnostics;
using System.Text;
using System.Net.Http.Headers;
using System.Web;
using System.Dynamic;
using System.Threading;
using System.ComponentModel;
using System.Xml.Serialization;
using CloudStorageLight.WpfClient.Properties;
using System.Security.Cryptography;
using System.Collections.Concurrent;
using DeltaCompressionDotNet.MsDelta;
using ViewMaker.Core.Wpf;
using ViewMaker;

namespace CloudStorageLight.WpfClient
{
    /// <summary>
    /// ストレージBlobのデータ操作管理
    /// </summary>
    public partial class BlobDataManager
    {

        #region files cahce

        protected const int CachedDays = 3;

        /// <summary>
        /// ファイル キャッシュ
        /// </summary>
        private ConcurrentDictionary<string, CachedFile> CacheFiles
        {
            get { return _CacheFiles; }
        }
        private ConcurrentDictionary<string, CachedFile> _CacheFiles = new ConcurrentDictionary<string, CachedFile>();


        /// <summary>
        /// キャッシュよりTreeItemを作成する
        /// </summary>
        /// <returns></returns>
        public List<TreeItem> LoadCachedFile()
        {
            var root = new DirectoryItem { Path = "/", LoadedChildren = true };
            root.Folders.Clear();
            foreach (var item in CacheFiles.Values)
            {
                if (!item.IsDownloaded) continue;
                AddFileItem(root, item.Path, item.LocalPath, item.LastModified, new FileInfo(item.LocalPath).Length);
            }

            return root.Folders.Cast<TreeItem>().Concat(root.Files.Cast<TreeItem>()).ToList();
        }

        private void AddFileItem(DirectoryItem root, string path, string localPath, DateTime lastModified, long size)
        {

            var current = root;
            var direItems = path.TrimStart('/').Split('/').ToList();
            foreach (var dire in direItems.Take(direItems.Count - 1))
            {
                var next = current.Folders.FirstOrDefault(x => x.Path == current.Path.TrimEnd('/') + "/" + dire + "/");
                if (next == null)
                {
                    var direItem = new DirectoryItem { Title = dire, Path = current.Path.TrimEnd('/') + "/" + dire + "/", LoadedChildren = true };
                    direItem.Folders.Clear();
                    current.Folders.Add(direItem);
                    current = direItem;
                }
                else
                {
                    current = next;
                }
            }
            var fileItem = new FileItem { Path = path, Title = GetFileName(path), AttachmentFileName = System.IO.Path.GetFileName(localPath), LastModified = lastModified, Length = size };
            current.Files.Add(fileItem);
        }



        /// <summary>
        /// キャッシュを取得
        /// </summary>
        /// <param name="filePath"></param>
        /// <returns></returns>
        public CachedFile GetCachedFile(string filePath)
        {
            return GetCachedFile(new FileItem { Path = filePath });
        }

        /// <summary>
        /// キャッシュを取得
        /// </summary>
        /// <param name="file"></param>
        /// <returns></returns>
        public CachedFile GetCachedFile(FileItem file)
        {
            RestoreCachedFile();
            var id = CreateCahcedId(file);
            if (CacheFiles.Keys.Contains(id)) return CacheFiles[id];
            return null;
        }

        /// <summary>
        /// キャッシュエントリの作成
        /// </summary>
        /// <param name="file"></param>
        /// <returns></returns>
        public CachedFile CreateCacheFileEntry(FileItem file)
        {
            var cache = GetCachedFile(file);
            if (cache != null) ClearCachedFile(file);
            var id = CreateCahcedId(file);
            var guid = Guid.NewGuid().ToString("d");
            var targetDire = Utils.ApplicationFolder + "\\Cache_" + guid;
            var localPath = Path.Combine(targetDire, file.AttachmentFileName);
            CacheFiles[id] = new CachedFile { Id = id, CacheFolder = targetDire, Path = file.Path, LocalPath = localPath, LastModified = file.LastModified };
            Directory.CreateDirectory(targetDire);
            var parentXml = Convert.ToBase64String(Encoding.UTF8.GetBytes(serverUrl + LoginUser)) + ".xml";
            File.WriteAllText(Path.Combine(targetDire, guid + ".txt"), serverUrl + "\n" + parentXml + "\n" + file.Id + "\n" + file.Path + "\n" + localPath);
            SaveCachedFile();
            return CacheFiles[id];
        }

        private string CreateCahcedId(string path)
        {
            return CreateCahcedId(new FileItem { Path = path });
        }

            /// <summary>
        /// キャッシュIDの作成
        /// </summary>
        /// <param name="file"></param>
        /// <returns></returns>
        private string CreateCahcedId(FileItem file)
        {
            if (file.AttachmentFileName == null || file.Path.EndsWith(file.AttachmentFileName))
            {
                return file.Path + "_" + file.Snapshot ?? "";
            }
            else
            {
                return file.Path + "_" + file.AttachmentFileName + "_" + file.Snapshot ?? "";
            }
        }

        /// <summary>
        /// サーバ情報の設定
        /// </summary>
        /// <param name="file"></param>
        /// <param name="info"></param>
        public void SetCacheDownloaded(FileItem file, DownloadInfo info)
        {
            var cachedFile = GetCachedFile(file);
            if (cachedFile == null) throw new ArgumentException("file");
            cachedFile.LocalFileCachedTime = new FileInfo(cachedFile.LocalPath).LastWriteTime;
            cachedFile.LastModified = file.LastModified;
            cachedFile.SnapshotTime = info.Version;
            SaveCachedFile();
            file.RaisePropertyChanged(null);
        }

        /// <summary>
        /// キャッシュ情報のクリア
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public CachedFile ClearCachedFile(string path)
        {
            return ClearCachedFile(new FileItem { Path = path });
        }

        /// <summary>
        /// キャッシュ情報のクリア
        /// </summary>
        /// <param name="file"></param>
        /// <returns></returns>
        public CachedFile ClearCachedFile(FileItem file)
        {
            var id = CreateCahcedId(file);
            if (!CacheFiles.Keys.Contains(id)) return null;
            var cacheFolder = CacheFiles[id].CacheFolder;
            if (cacheFolder.StartsWith(Utils.ApplicationFolder) && Directory.Exists(cacheFolder))
            {
                ForceDeleteDirectory(cacheFolder);
            }

            CachedFile cahcework;
            CacheFiles.TryRemove(id, out cahcework);
            SaveCachedFile();
            return null;
        }


        /// <summary>
        /// キャッシュ情報の保存
        /// </summary>
        public void SaveCachedFile()
        {
            lock (CacheFiles)
            {
                var seri = new XmlSerializer(typeof(List<CachedFile>));
                var file = Path.Combine(Utils.ApplicationFolder, "CachedFile_" + Convert.ToBase64String(Encoding.UTF8.GetBytes(serverUrl + LoginUser)) + ".xml");
                using (var st = new StreamWriter(file))
                {

                    seri.Serialize(st, CacheFiles.Values.ToList());
                }
                cacheFileLastUpdate = File.GetLastWriteTimeUtc(file);
            }
            OnRecentChanged(null);
        }

        private DateTime cacheFileLastUpdate;
        /// <summary>
        /// キャッシュ情報の読み出し
        /// </summary>
        public void RestoreCachedFile(bool rebuild = false)
        {
            Exception firstException = null;
            var seri = new XmlSerializer(typeof(List<CachedFile>));
            var file = Path.Combine(Utils.ApplicationFolder, "CachedFile_" + Convert.ToBase64String(Encoding.UTF8.GetBytes(serverUrl + LoginUser)) + ".xml");
            if (File.Exists(file))
            {
                var fileWriteTime = File.GetLastWriteTimeUtc(file);
                if (cacheFileLastUpdate == fileWriteTime) return;
                try
                {
                    using (var st = new StreamReader(file))
                    {
                        _CacheFiles = new ConcurrentDictionary<string, CachedFile>(
                            ((List<CachedFile>)seri.Deserialize(st)).ToDictionary(x => x.Id, x => x));
                    }
                    cacheFileLastUpdate = fileWriteTime;
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.ToString());
                }
            }

            if (rebuild)
            {
                var parentXml = Convert.ToBase64String(Encoding.UTF8.GetBytes(serverUrl + LoginUser)) + ".xml";

                foreach (var dire in Directory.GetDirectories(Utils.ApplicationFolder, "Cache_*"))
                {
                    if (CacheFiles.Any(x => x.Value.CacheFolder == dire)) continue;
                    var infoFileName = Path.GetFileName(dire).Substring("Cache_".Length);
                    var infoFilePath = Path.Combine(dire, infoFileName + ".txt");
                    if (File.Exists(infoFilePath))
                    {
                        var lines = File.ReadLines(infoFilePath).ToList();
                        if (lines.Count < 5) continue;
                        if (lines[1] == parentXml)
                        {
                            var path = lines[3];
                            var id = CreateCahcedId(path);
                            var filePath = lines[4];
                            if (File.Exists(filePath))
                            {
                                var info = new FileInfo(filePath);
                                CacheFiles[id] = new CachedFile {
                                    Id = id,
                                    CacheFolder = dire,
                                    Path = path,
                                    LocalPath = filePath,
                                    LastModified = info.LastWriteTimeUtc,
                                    LocalFileCachedTime = info.LastWriteTime,
                                    LastAccessTime = info.LastAccessTime,
                                };
                            }
                        }
                    }
                }
            }

            //キャッシュしたファイルが無くなっている場合は削除
            var isDirty = false;
            foreach (var keyval in CacheFiles.ToList())
            {
                if (!File.Exists(keyval.Value.LocalPath))
                {
                    try
                    {
                        ClearCachedFile(keyval.Value.Path);
                    }
                    catch (Exception ex)
                    {
                        firstException = ex;
                    }
                }
            }
            //if (firstException != null) throw firstException;

            //期限の切れたキャッシュも削除
            foreach (var item in CacheFiles.Where(x => x.Value.LastAccessTime.AddDays(CachedDays) < DateTime.Now))
            {
                if (item.Value.IsDirty) continue;
                try
                {
                    ClearCachedFile(item.Value.Path);
                }
                catch (Exception ex)
                {
                    firstException = ex;
                }
            }
            //if (firstException != null) throw firstException;

            if (isDirty)
            {
                SaveCachedFile();
            }

        }


        public string GetLocalFilePath(FileItem item)
        {
            var cache = GetCachedFile(item);
            if (cache != null) return cache.LocalPath;
            return null;
        }

        public List<CachedFile> GetRecentFiles()
        {
            var list = CacheFiles.OrderByDescending(x => x.Value.LastAccessTime).Select(x => x.Value).ToList();
            return list;
        }

        public List<CachedFile> GetEditingFiles()
        {
            var list = CacheFiles.Where(x => x.Value.IsEditing).OrderBy(x => x.Value.Path).Select(x => x.Value).ToList();
            return list;
        }

        public CachedFile RenameCachedFile(FileItem file, string newPath)
        {
            var cache = GetCachedFile(file);
            if (cache == null) return null;

            //移動先のキャッシュを作成
            var newCache = GetCachedFile(newPath);
            if (newCache == null)
            {
                newCache = CreateCacheFileEntry(new FileItem { Path = newPath, AttachmentFileName = GetFileName(newPath) });
                Directory.CreateDirectory(Path.GetDirectoryName(newCache.LocalPath));
            }
            newCache.LastModified = cache.LastModified;
            if (File.Exists(newCache.LocalPath)) File.Delete(newCache.LocalPath);

            //ファイルの移動
            if (File.Exists(cache.LocalPath))
            {
                File.Move(cache.LocalPath, newCache.LocalPath);
                newCache.LocalFileCachedTime = new FileInfo(newCache.LocalPath).LastWriteTime;
            }

            //元のキャッシュを削除
            ClearCachedFile(file);

            SaveCachedFile();
            file.RaisePropertyChanged(null);
            return cache;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="days"></param>
        public void PurgeCachedFile(int days = 30)
        {
            var list = new List<string>();
            var seri = new XmlSerializer(typeof(List<CachedFile>));
            foreach (var cacheDb in Directory.GetFiles(Utils.ApplicationFolder, "CachedFile_*.xml"))
            {
                try
                {
                    using (var st = new StreamReader(cacheDb))
                    {
                        var data = new ConcurrentDictionary<string, CachedFile>(
                            ((List<CachedFile>)seri.Deserialize(st)).ToDictionary(x => x.Id, x => x));
                        list.AddRange(data.Values.Where(x => x.IsDirty && x.LastAccessTime.AddDays(CachedDays) < DateTime.Now).Select(x => x.CacheFolder));
                    }

                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.ToString());
                }
            }

            foreach (var dire in Directory.GetDirectories(Utils.ApplicationFolder, "Cache_*"))
            {
                if (list.Any(x => string.Equals(x, dire, StringComparison.InvariantCultureIgnoreCase))) continue;
                var purge = true;
                foreach (var file in Directory.GetFiles(dire))
                {
                    var lastAccess = File.GetLastAccessTimeUtc(file);
                    if (lastAccess.AddDays(days) > DateTime.UtcNow)
                    {
                        purge = false;
                    }
                }
                if (purge)
                {
                    ForceDeleteDirectory(dire);
                }
            }
        }

        #endregion


        public void SaveSettings(LocalSettings data)
        {
            var seri = new XmlSerializer(typeof(LocalSettings));
            var file = Path.Combine(Utils.ApplicationFolder, "Settings.xml");
            using (var st = new StreamWriter(file))
            {
                seri.Serialize(st, data);
            }
        }

        public LocalSettings LoadSettings()
        {
            LocalSettings data = null;
            var seri = new XmlSerializer(typeof(LocalSettings));
            var file = Path.Combine(Utils.ApplicationFolder, "Settings.xml");
            if (File.Exists(file))
            {
                try
                {
                    using (var st = new StreamReader(file))
                    {
                        data = (LocalSettings)seri.Deserialize(st);
                    }

                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.ToString());
                }
            }
            if (data == null) data = new LocalSettings();
            return data;
        }

        public CachedUser LoadCachedUser(string serviceUrl, string email)
        {
            List<CachedUser> cachedUsers = LoadCachedUser();
            return cachedUsers.Where(x => x.Email == email && x.ServiceUrl == serviceUrl).FirstOrDefault();
        }

        #region users cache

        private void SaveCachedUser(string serviceUrl, string email, string password, bool rememberMe)
        {
            List<CachedUser> cachedUsers = LoadCachedUser();

            var seri = new XmlSerializer(typeof(List<CachedUser>));
            var file = Path.Combine(Utils.ApplicationFolder, "CachedUser.xml");
            var user = cachedUsers.FirstOrDefault(x => x.Email == email && x.ServiceUrl == serviceUrl);
            if (user != null)
            {
                user.EncryptedPassword = EncryptedPassword(password);
                user.RememberMe = rememberMe;
            }
            else
            {
                cachedUsers.Add(new CachedUser { ServiceUrl = serviceUrl, Email = email, EncryptedPassword = EncryptedPassword(password), RememberMe = rememberMe });
            }
            
            using (var st = new StreamWriter(file))
            {
                seri.Serialize(st, cachedUsers);
            }
        }


        private List<CachedUser> LoadCachedUser()
        {
            List<CachedUser> cachedUsers = null;
            var seri = new XmlSerializer(typeof(List<CachedUser>));
            var file = Path.Combine(Utils.ApplicationFolder, "CachedUser.xml");
            if (File.Exists(file))
            {
                try
                {
                    using (var st = new StreamReader(file))
                    {
                        cachedUsers = ((List<CachedUser>)seri.Deserialize(st));
                    }

                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.ToString());
                }
            }
            if (cachedUsers == null) cachedUsers = new List<CachedUser>();
            return cachedUsers;
        }

        #endregion

        #region client token

        private void SaveClientToken(string token)
        {
            if (!File.Exists(Utils.ApplicationFolder)) Directory.CreateDirectory(Utils.ApplicationFolder);
            File.WriteAllText(GetClientTokenFileName(), EncryptedPassword(token));
        }

        private string LoadClientToken()
        {
            try
            {
                var file = GetClientTokenFileName();
                if (File.Exists(file)) return DecryptedPassword(File.ReadAllText(file));
                return null;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                return null;
            }
        }

        private string GetClientTokenFileName()
        {
            return Path.Combine(Utils.ApplicationFolder, "ClientToken" + Convert.ToBase64String(Encoding.UTF8.GetBytes(serverUrl)) + ".txt");
        }
        #endregion


        #region favorite

        public void SaveFavorites(List<string> favorites)
        {
            if (!File.Exists(Utils.ApplicationFolder)) Directory.CreateDirectory(Utils.ApplicationFolder);
            File.WriteAllLines(GetFavoriteFileName(),favorites);
        }

        public List<string> LoadFavorites()
        {
            try
            {
                var file = GetFavoriteFileName();
                if (File.Exists(file))
                {
                    return File.ReadLines(file).ToList();
                }
                return new List<string>();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                return new List<string>();
            }
        }

        private string GetFavoriteFileName()
        {
            return Path.Combine(Utils.ApplicationFolder, "Favorite" + Convert.ToBase64String(Encoding.UTF8.GetBytes(serverUrl)) + ".txt");
        }
        #endregion

        private void ForceDeleteDirectory(string path)
        {
            var directory = new DirectoryInfo(path) { Attributes = FileAttributes.Normal };

            foreach (var info in directory.GetFileSystemInfos("*", SearchOption.AllDirectories))
            {
                info.Attributes = FileAttributes.Normal;
            }

            directory.Delete(true);
        }

        private void ForceDeleteFile(string path)
        {
            var file = new FileInfo(path) { Attributes = FileAttributes.Normal };
            file.Delete();
        }

        private async Task LoadFileFromCacheAsync(Stream target, string cacheId)
        {
            if (CacheFiles.ContainsKey(cacheId) && File.Exists(CacheFiles[cacheId].LocalPath))
            {
                using (var st = File.OpenRead(CacheFiles[cacheId].LocalPath))
                {
                    await st.CopyToAsync(target);
                }
            }
            else
            {
                throw new ApplicationException(Resources.MsgNoCahcedFile);
            }
        }
        private bool TryLocalCahceLogin(string serviceUrl, string mail, string pass)
        {
            var user = LoadCachedUser().FirstOrDefault(x => x.Email == mail && x.ServiceUrl == serverUrl);
            if (user == null) return false;
            return DecryptedPassword(user.EncryptedPassword) == pass;
        }

        private Dictionary<string, string> CloudAccountInfo
        {
            get { return cloudAccountInfo; }
        }
        private Dictionary<string, string> cloudAccountInfo;


        private static byte[] entropy = new byte[] { 0x88, 0xa0, 0x22, 0x04 };

        private static string EncryptedPassword(string password)
        {
            var encr = ProtectedData.Protect(Encoding.UTF8.GetBytes(password), entropy, DataProtectionScope.CurrentUser);
            return Convert.ToBase64String(encr);
        }

        internal static string DecryptedPassword(string password)
        {
            var unenc = ProtectedData.Unprotect(Convert.FromBase64String(password), entropy, DataProtectionScope.CurrentUser);
            return Encoding.UTF8.GetString(unenc);
        }


    }


    public class CachedUser
    {
        public string ServiceUrl { get; set; }
        public string Email { get; set; }
        public string EncryptedPassword { get; set; }
        public bool RememberMe { get; set; }
    }
}
